#!/usr/bin/env python


__license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'

import errno
import os
import shutil
import stat
from queue import Empty, LifoQueue
from threading import Thread

from qt.core import QHBoxLayout, QLabel, QObject, QSize, Qt, QWidget, pyqtSignal

from calibre.constants import iswindows
from calibre.gui2.progress_indicator import ProgressIndicator
from calibre.ptempfile import PersistentTemporaryFile
from calibre.utils import join_with_timeout
from calibre.utils.filenames import atomic_rename, format_permissions


def save_dir_container(container, path):
    if not os.path.exists(path):
        os.makedirs(path)
    if not os.path.isdir(path):
        raise ValueError(f'{path} is not a folder, cannot save a directory based container to it')
    container.commit(path)


def save_container(container, path):
    if container.is_dir:
        return save_dir_container(container, path)
    temp = PersistentTemporaryFile(
        prefix=('_' if iswindows else '.'), suffix=os.path.splitext(path)[1], dir=os.path.dirname(path))
    if hasattr(os, 'fchmod'):
        # Ensure file permissions and owner information is preserved
        fno = temp.fileno()
        st = None
        try:
            st = os.stat(path)
        except OSError as err:
            if err.errno != errno.ENOENT:
                raise
            # path may not exist if we are saving a copy, in which case we use
            # the metadata from the original book
            try:
                st = os.stat(container.path_to_ebook)
            except OSError as err:
                if err.errno != errno.ENOENT:
                    raise
                # Somebody deleted the original file
        if st is not None:
            try:
                os.fchmod(fno, st.st_mode | stat.S_IWUSR)
            except OSError as err:
                if err.errno != errno.EPERM:
                    raise
                raise OSError(f'Failed to change permissions of {temp.name} to {oct(st.st_mode)} ({format_permissions(st.st_mode)}), '
                              f'with error: {errno.errorcode[err.errno]}. Most likely the {os.path.dirname(temp.name)} directory has a restrictive umask')
            try:
                os.fchown(fno, st.st_uid, st.st_gid)
            except OSError as err:
                if err.errno not in (errno.EPERM, errno.EACCES):
                    # ignore chown failure as user could be editing file belonging
                    # to a different user, in which case we really can't do anything
                    # about it short of making the file update non-atomic
                    raise

    temp.close()
    temp = temp.name
    try:
        container.commit(temp)
        atomic_rename(temp, path)
    finally:
        if os.path.exists(temp):
            os.remove(temp)


def send_message(msg=''):
    if msg:
        from calibre.gui2.listener import send_message_in_process
        send_message_in_process('bookedited:'+msg)


def find_first_existing_ancestor(path):
    while path and not os.path.exists(path):
        npath = os.path.dirname(path)
        if npath == path:
            break
        path = npath
    return path


class SaveWidget(QWidget):

    def __init__(self, parent=None):
        QWidget.__init__(self, parent)
        self.l = l = QHBoxLayout(self)
        self.setLayout(l)
        self.label = QLabel('')
        self.pi = ProgressIndicator(self, 24)
        l.addWidget(self.label)
        l.addWidget(self.pi)
        l.setContentsMargins(0, 0, 0, 0)
        self.pi.setVisible(False)
        self.stop()

    def start(self):
        self.pi.setDisplaySize(QSize(self.label.height(), self.label.height()))
        self.pi.setVisible(True)
        self.pi.startAnimation()
        self.label.setText(_('Saving...'))

    def stop(self):
        self.pi.setVisible(False)
        self.pi.stopAnimation()
        self.label.setText('')


class SaveManager(QObject):

    start_save = pyqtSignal()
    report_error = pyqtSignal(object)
    save_done = pyqtSignal()
    check_for_completion = pyqtSignal()

    def __init__(self, parent, notify=None):
        QObject.__init__(self, parent)
        self.count = 0
        self.last_saved = -1
        self.requests = LifoQueue()
        self.notify_requests = LifoQueue()
        self.notify_data = notify
        t = Thread(name='save-thread', target=self.run)
        t.daemon = True
        t.start()
        t = Thread(name='notify-thread', target=self.notify_calibre)
        t.daemon = True
        t.start()
        self.status_widget = w = SaveWidget(parent)
        self.start_save.connect(w.start, type=Qt.ConnectionType.QueuedConnection)
        self.save_done.connect(w.stop, type=Qt.ConnectionType.QueuedConnection)

    def schedule(self, tdir, container):
        self.count += 1
        self.requests.put((self.count, tdir, container))

    def run(self):
        while True:
            x = self.requests.get()
            if x is None:
                self.requests.task_done()
                self.__empty_queue()
                break
            error_occurred = True
            try:
                count, tdir, container = x
                error_occurred = self.process_save(count, tdir, container)
            except Exception:
                import traceback
                traceback.print_exc()
            finally:
                self.requests.task_done()
            if not error_occurred:
                self.check_for_completion.emit()

    def notify_calibre(self):
        while True:
            if not self.notify_requests.get():
                break
            send_message(self.notify_data)

    def clear_notify_data(self):
        self.notify_data = None

    def __empty_queue(self):
        ' Only to be used during shutdown '
        while True:
            try:
                self.requests.get_nowait()
            except Empty:
                break
            else:
                self.requests.task_done()

    def process_save(self, count, tdir, container):
        if count <= self.last_saved:
            shutil.rmtree(tdir, ignore_errors=True)
            return
        self.last_saved = count
        self.start_save.emit()
        error_occurred = False
        try:
            self.do_save(tdir, container)
        except Exception:
            import traceback
            self.report_error.emit(traceback.format_exc())
            error_occurred = True
        self.save_done.emit()
        if self.notify_data:
            self.notify_requests.put(True)
        return error_occurred

    def do_save(self, tdir, container):
        try:
            save_container(container, container.path_to_ebook)
        finally:
            shutil.rmtree(tdir, ignore_errors=True)

    @property
    def has_tasks(self):
        return bool(self.requests.unfinished_tasks)

    def wait(self, timeout=30):
        if timeout is None:
            self.requests.join()
        else:
            try:
                join_with_timeout(self.requests, timeout)
            except RuntimeError:
                return False
        return True

    def shutdown(self):
        self.requests.put(None)
        self.notify_requests.put(None)
